import assert from "assert";
import { afterEach, describe, expect, test } from "bun:test";
import { readFileSync, writeFileSync } from "fs";
import { bunEnv, bunExe, tempDirWithFiles, tempDirWithFilesAnon } from "harness";
import path, { join } from "path";
import { buildNoThrow } from "./buildNoThrow";

describe("Bun.build", () => {
  test("css works", async () => {
    const dir = tempDirWithFiles("bun-build-api-css", {
      "a.css": `
        @import "./b.css";

        .hi {
          color: red;
        }
      `,
      "b.css": `
        .hello {
          color: blue;
        }
      `,
    });

    const build = await Bun.build({
      entrypoints: [join(dir, "a.css")],
      minify: true,
    });

    expect(build.outputs).toHaveLength(1);
    expect(build.outputs[0].kind).toBe("asset");
    expect(await build.outputs[0].text()).toEqualIgnoringWhitespace(".hello{color:#00f}.hi{color:red}\n");
  });

  test("bytecode works", async () => {
    const dir = tempDirWithFiles("bun-build-api-bytecode", {
      "package.json": `{}`,
      "index.ts": `
        export function hello() {
          return "world";
        }

        console.log(hello());
      `,
      out: {
        "hmm.js": "hmm",
      },
    });

    const build = await Bun.build({
      entrypoints: [join(dir, "index.ts")],
      outdir: join(dir, "out"),
      target: "bun",
      bytecode: true,
    });

    expect(build.outputs).toHaveLength(2);
    expect(build.outputs[0].kind).toBe("entry-point");
    expect(build.outputs[1].kind).toBe("bytecode");
    expect([build.outputs[0].path]).toRun("world\n");
  });

  test("passing undefined doesnt segfault", () => {
    try {
      // @ts-ignore
      Bun.build();
    } catch (error) {
      return;
    }
    throw new Error("should have thrown");
  });

  // https://github.com/oven-sh/bun/issues/12818
  test("sourcemap + build error crash case", async () => {
    const dir = tempDirWithFiles("build", {
      "/src/file1.ts": `
        import { A } from './dir';
        console.log(A);
      `,
      "/src/dir/index.ts": `
        import { B } from "./file3";
        export const A = [B]
      `,
      "/src/dir/file3.ts": `
        import { C } from "../file1"; // error
        export const B = C;
      `,
      "/src/package.json": `
        { "type": "module" }
      `,
      "/src/tsconfig.json": `
        {
          "extends": "../tsconfig.json",
          "compilerOptions": {
              "target": "ESNext",
              "module": "ESNext",
              "types": []
          }
        }
      `,
    });
    const y = await buildNoThrow({
      entrypoints: [join(dir, "src/file1.ts")],
      outdir: join(dir, "out"),
      sourcemap: "external",
      external: ["@minecraft"],
    });
  });

  test("invalid options throws", async () => {
    expect(() => Bun.build({} as any)).toThrow();
    expect(() =>
      Bun.build({
        entrypoints: [],
      } as any),
    ).toThrow();
    expect(() =>
      Bun.build({
        entrypoints: ["hello"],
        format: "invalid",
      } as any),
    ).toThrow();
    expect(() =>
      Bun.build({
        entrypoints: ["hello"],
        target: "invalid",
      } as any),
    ).toThrow();
    expect(() =>
      Bun.build({
        entrypoints: ["hello"],
        sourcemap: "invalid",
      } as any),
    ).toThrow();
  });

  test("returns errors properly", async () => {
    Bun.gc(true);
    const build = await buildNoThrow({
      entrypoints: [join(import.meta.dir, "does-not-exist.ts")],
    });
    expect(build.outputs).toHaveLength(0);
    expect(build.logs).toHaveLength(1);
    expect(build.logs[0]).toBeInstanceOf(BuildMessage);
    expect(build.logs[0].message).toMatch(/ModuleNotFound/);
    expect(build.logs[0].name).toBe("BuildMessage");
    expect(build.logs[0].position).toEqual(null);
    expect(build.logs[0].level).toEqual("error");
    Bun.gc(true);
  });

  test("errors are thrown", async () => {
    Bun.gc(true);
    try {
      await Bun.build({
        entrypoints: [join(import.meta.dir, "does-not-exist.ts")],
      });
      expect.unreachable();
    } catch (e) {
      assert(e instanceof AggregateError);
      expect(e.errors).toHaveLength(1);
      expect(e.errors[0]).toBeInstanceOf(BuildMessage);
      expect(e.errors[0].message).toMatch(/ModuleNotFound/);
      expect(e.errors[0].name).toBe("BuildMessage");
      expect(e.errors[0].position).toEqual(null);
      expect(e.errors[0].level).toEqual("error");
      Bun.gc(true);
    }
  });

  test("returns output files", async () => {
    Bun.gc(true);
    const build = await Bun.build({
      entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
    });
    expect(build.outputs).toHaveLength(1);
    expect(build.logs).toHaveLength(0);
    Bun.gc(true);
  });

  test("Bun.write(BuildArtifact)", async () => {
    Bun.gc(true);
    const tmpdir = tempDirWithFiles("bun-build-api-write", {
      "package.json": `{}`,
    });
    const x = await Bun.build({
      entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
    });
    await Bun.write(path.join(tmpdir, "index.js"), x.outputs[0]);
    expect(readFileSync(path.join(tmpdir, "index.js"), "utf-8")).toMatchSnapshot();
    Bun.gc(true);
  });

  test("rebuilding busts the directory entries cache", () => {
    Bun.gc(true);
    const tmpdir = tempDirWithFiles("rebuild-bust-dirent-cache", {
      "package.json": `{}`,
    });

    const { exitCode, stderr } = Bun.spawnSync({
      cmd: [bunExe(), join(import.meta.dir, "fixtures", "bundler-reloader-script.ts")],
      env: { ...bunEnv, BUNDLER_RELOADER_SCRIPT_TMP_DIR: tmpdir },
      stderr: "pipe",
      stdout: "inherit",
    });
    if (stderr.byteLength > 0) {
      throw new Error(stderr.toString());
    }
    expect(exitCode).toBe(0);
    Bun.gc(true);
  });

  test("outdir + reading out blobs works", async () => {
    Bun.gc(true);
    const fixture = tempDirWithFiles("build-outdir", {
      "package.json": `{}`,
    });
    const x = await Bun.build({
      entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
      outdir: fixture,
    });
    expect(await x.outputs.values().next().value?.text()).toMatchSnapshot();
    Bun.gc(true);
  });

  test("BuildArtifact properties", async () => {
    Bun.gc(true);
    const outdir = tempDirWithFiles("build-artifact-properties", {
      "package.json": `{}`,
    });
    const x = await Bun.build({
      entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
      outdir,
    });
    console.log(await x.outputs[0].text());
    const [blob] = x.outputs;
    expect(blob).toBeTruthy();
    expect(blob.type).toBe("text/javascript;charset=utf-8");
    expect(blob.size).toBeGreaterThan(1);
    expect(path.relative(outdir, blob.path)).toBe("index.js");
    expect(blob.hash).toBeTruthy();
    expect(blob.hash).toMatchSnapshot("hash");
    expect(blob.kind).toBe("entry-point");
    expect(blob.loader).toBe("jsx");
    expect(blob.sourcemap).toBe(null);
    Bun.gc(true);
  });

  test("BuildArtifact properties + entry.naming", async () => {
    Bun.gc(true);
    const outdir = tempDirWithFiles("build-artifact-properties-entry-naming", {
      "package.json": `{}`,
    });
    const x = await Bun.build({
      entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
      naming: {
        entry: "hello",
      },
      outdir,
    });
    const [blob] = x.outputs;
    expect(blob).toBeTruthy();
    expect(blob.type).toBe("text/javascript;charset=utf-8");
    expect(blob.size).toBeGreaterThan(1);
    expect(path.relative(outdir, blob.path)).toBe("hello");
    expect(blob.hash).toBeTruthy();
    expect(blob.hash).toMatchSnapshot("hash");
    expect(blob.kind).toBe("entry-point");
    expect(blob.loader).toBe("jsx");
    expect(blob.sourcemap).toBe(null);
    Bun.gc(true);
  });

  test("BuildArtifact properties sourcemap", async () => {
    Bun.gc(true);
    const outdir = tempDirWithFiles("build-artifact-properties-sourcemap", {
      "package.json": `{}`,
    });
    const x = await Bun.build({
      entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
      sourcemap: "external",
      outdir,
    });
    const [blob, map] = x.outputs;
    expect(blob.type).toBe("text/javascript;charset=utf-8");
    expect(blob.size).toBeGreaterThan(1);
    expect(path.relative(outdir, blob.path)).toBe("index.js");
    expect(blob.hash).toBeTruthy();
    expect(blob.hash).toMatchSnapshot("hash index.js");
    expect(blob.kind).toBe("entry-point");
    expect(blob.loader).toBe("jsx");
    expect(blob.sourcemap).toBe(map);

    expect(map.type).toBe("application/json;charset=utf-8");
    expect(map.size).toBeGreaterThan(1);
    expect(path.relative(outdir, map.path)).toBe("index.js.map");
    expect(map.hash).toBeTruthy();
    expect(map.hash).toMatchSnapshot("hash index.js.map");
    expect(map.kind).toBe("sourcemap");
    expect(map.loader).toBe("file");
    expect(map.sourcemap).toBe(null);
    Bun.gc(true);
  });

  // test("BuildArtifact properties splitting", async () => {
  //   Bun.gc(true);
  //   const x = await Bun.build({
  //     entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
  //     splitting: true,
  //   });
  //   expect(x.outputs).toHaveLength(2);
  //   const [indexBlob, chunkBlob] = x.outputs;

  //   expect(indexBlob).toBeTruthy();
  //   expect(indexBlob.type).toBe("text/javascript;charset=utf-8");
  //   expect(indexBlob.size).toBeGreaterThan(1);
  //   expect(indexBlob.path).toBe("/index.js");
  //   expect(indexBlob.hash).toBeTruthy();
  //   expect(indexBlob.hash).toMatchSnapshot("hash index.js");
  //   expect(indexBlob.kind).toBe("entry-point");
  //   expect(indexBlob.loader).toBe("jsx");
  //   expect(indexBlob.sourcemap).toBe(null);

  //   expect(chunkBlob).toBeTruthy();
  //   expect(chunkBlob.type).toBe("text/javascript;charset=utf-8");
  //   expect(chunkBlob.size).toBeGreaterThan(1);
  //   expect(chunkBlob.path).toBe(`/foo-${chunkBlob.hash}.js`);
  //   expect(chunkBlob.hash).toBeTruthy();
  //   expect(chunkBlob.hash).toMatchSnapshot("hash foo.js");
  //   expect(chunkBlob.kind).toBe("chunk");
  //   expect(chunkBlob.loader).toBe("jsx");
  //   expect(chunkBlob.sourcemap).toBe(null);
  //   Bun.gc(true);
  // });

  test("new Response(BuildArtifact) sets content type", async () => {
    const x = await Bun.build({
      entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
      outdir: tempDirWithFiles("response-buildartifact", {}),
    });
    const response = new Response(x.outputs[0]);
    expect(response.headers.get("content-type")).toBe("text/javascript;charset=utf-8");
    expect(await response.text()).toMatchSnapshot("response text");
  });

  test.todo("new Response(BuildArtifact) sets etag", async () => {
    const x = await Bun.build({
      entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")],
      outdir: tempDirWithFiles("response-buildartifact-etag", {}),
    });
    const response = new Response(x.outputs[0]);
    expect(response.headers.get("etag")).toBeTruthy();
    expect(response.headers.get("etag")).toMatchSnapshot("content-etag");
  });

  // test("BuildArtifact with assets", async () => {
  //   const x = await Bun.build({
  //     entrypoints: [join(import.meta.dir, "./fixtures/with-assets/index.js")],
  //     loader: {
  //       ".blob": "file",
  //       ".png": "file",
  //     },
  //   });
  //   console.log(x);
  //   const [blob, asset] = x.outputs;
  //   expect(blob).toBeTruthy();
  //   expect(blob instanceof Blob).toBe(true);
  //   expect(blob.type).toBe("text/javascript;charset=utf-8");
  //   expect(blob.size).toBeGreaterThan(1);
  //   expect(blob.path).toBe("/index.js");
  //   expect(blob.hash).toBeTruthy();
  //   expect(blob.hash).toMatchSnapshot();
  //   expect(blob.kind).toBe("entry-point");
  //   expect(blob.loader).toBe("jsx");
  //   expect(blob.sourcemap).toBe(null);
  //   throw new Error("test was not fully written");
  // });

  test("errors are returned as an array", async () => {
    const x = await buildNoThrow({
      entrypoints: [join(import.meta.dir, "does-not-exist.ts")],
      outdir: tempDirWithFiles("errors-are-returned-as-an-array", {}),
    });
    expect(x.success).toBe(false);
    expect(x.logs).toHaveLength(1);
    expect(x.logs[0].message).toMatch(/ModuleNotFound/);
    expect(x.logs[0].name).toBe("BuildMessage");
    expect(x.logs[0].position).toEqual(null);
  });

  test("warnings do not fail a build", async () => {
    const x = await Bun.build({
      entrypoints: [join(import.meta.dir, "./fixtures/jsx-warning/index.jsx")],
      outdir: tempDirWithFiles("warnings-do-not-fail-a-build", {}),
    });
    expect(x.success).toBe(true);
    expect(x.logs).toHaveLength(1);
    expect(x.logs[0].message).toBe(
      '"key" prop after a {...spread} is deprecated in JSX. Falling back to classic runtime.',
    );
    expect(x.logs[0].name).toBe("BuildMessage");
    expect(x.logs[0].position).toBeTruthy();
  });

  test("module() throws error", async () => {
    expect(() =>
      Bun.build({
        entrypoints: [join(import.meta.dir, "./fixtures/trivial/bundle-ws.ts")],
        plugins: [
          {
            name: "test",
            setup: b => {
              b.module("ad", () => {
                return {
                  exports: {
                    hello: "world",
                  },
                  loader: "object",
                };
              });
            },
          },
        ],
      }),
    ).toThrow();
  });

  test("non-object plugins throw invalid argument errors", () => {
    for (const plugin of [null, undefined, 1, "hello", true, false, Symbol.for("hello")]) {
      expect(() => {
        Bun.build({
          entrypoints: [join(import.meta.dir, "./fixtures/trivial/bundle-ws.ts")],
          plugins: [
            // @ts-expect-error
            plugin,
          ],
        });
      }).toThrow("Expected plugin to be an object");
    }
  });

  test("hash considers cross chunk imports", async () => {
    Bun.gc(true);
    const fixture = tempDirWithFiles("build-hash-cross-chunk-imports", {
      "entry1.ts": `
        import { bar } from './bar'
        export const entry1 = () => {
          console.log('FOO')
          bar()
        }
      `,
      "entry2.ts": `
        import { bar } from './bar'
        export const entry1 = () => {
          console.log('FOO')
          bar()
        }
      `,
      "bar.ts": `
        export const bar = () => {
          console.log('BAR')
        }
      `,
    });
    const first = await Bun.build({
      entrypoints: [join(fixture, "entry1.ts"), join(fixture, "entry2.ts")],
      outdir: join(fixture, "out"),
      target: "browser",
      splitting: true,
      minify: false,
      naming: "[dir]/[name]-[hash].[ext]",
    });
    if (!first.success) throw new AggregateError(first.logs);
    expect(first.outputs.length).toBe(3);

    writeFileSync(join(fixture, "bar.ts"), readFileSync(join(fixture, "bar.ts"), "utf8").replace("BAR", "BAZ"));

    const second = await Bun.build({
      entrypoints: [join(fixture, "entry1.ts"), join(fixture, "entry2.ts")],
      outdir: join(fixture, "out2"),
      target: "browser",
      splitting: true,
      minify: false,
      naming: "[dir]/[name]-[hash].[ext]",
    });
    if (!second.success) throw new AggregateError(second.logs);
    expect(second.outputs.length).toBe(3);

    const totalUniqueHashes = new Set();
    const allFiles = [...first.outputs, ...second.outputs];
    for (const out of allFiles) totalUniqueHashes.add(out.hash);

    expect(
      totalUniqueHashes.size,
      "number of unique hashes should be 6: three per bundle. the changed foo.ts affects all chunks",
    ).toBe(6);

    // ensure that the hashes are in the path
    for (const out of allFiles) {
      expect(out.path).toInclude(out.hash!);
    }

    Bun.gc(true);
  });

  test("ignoreDCEAnnotations works", async () => {
    const fixture = tempDirWithFiles("build-ignore-dce-annotations", {
      "package.json": `{}`,
      "entry.ts": `
        /* @__PURE__ */ console.log(1)
      `,
    });

    const bundle = await Bun.build({
      entrypoints: [join(fixture, "entry.ts")],
      ignoreDCEAnnotations: true,
      minify: true,
      outdir: path.join(fixture, "out"),
    });
    if (!bundle.success) throw new AggregateError(bundle.logs);

    expect(await bundle.outputs[0].text()).toBe("console.log(1);\n");
  });

  test("emitDCEAnnotations works", async () => {
    const fixture = tempDirWithFiles("build-emit-dce-annotations", {
      "package.json": `{}`,
      "entry.ts": `
        export const OUT = /* @__PURE__ */ console.log(1)
      `,
    });

    const bundle = await Bun.build({
      entrypoints: [join(fixture, "entry.ts")],
      emitDCEAnnotations: true,
      minify: true,
      outdir: path.join(fixture, "out"),
    });
    if (!bundle.success) throw new AggregateError(bundle.logs);

    expect(await bundle.outputs[0].text()).toBe("var o=/*@__PURE__*/console.log(1);export{o as OUT};\n");
  });

  test("you can write onLoad and onResolve plugins using the 'html' loader, and it includes script and link tags as bundled entrypoints", async () => {
    const fixture = tempDirWithFiles("build-html-plugins", {
      "index.html": `
        <!DOCTYPE html>
        <html>
          <head>
            <link rel="stylesheet" href="./style.css">
            <script src="./script.js"></script>
          </head>
        </html>
      `,
      "style.css": ".foo { color: red; }",

      // Check we actually do bundle the script
      "script.js": "console.log(1 + 2)",
    });

    let onLoadCalled = false;
    let onResolveCalled = false;

    const build = await Bun.build({
      entrypoints: [join(fixture, "index.html")],
      minify: {
        syntax: true,
      },
      plugins: [
        {
          name: "test-plugin",
          setup(build) {
            build.onLoad({ filter: /\.html$/ }, async args => {
              onLoadCalled = true;
              const contents = await Bun.file(args.path).text();
              return {
                contents: contents.replace("</head>", "<meta name='injected-by-plugin' content='true'></head>"),
                loader: "html",
              };
            });

            build.onResolve({ filter: /\.(js|css)$/ }, args => {
              onResolveCalled = true;
              return {
                path: join(fixture, args.path),
                namespace: "file",
              };
            });
          },
        },
      ],
    });

    expect(build.success).toBe(true);
    expect(onLoadCalled).toBe(true);
    expect(onResolveCalled).toBe(true);

    // Should have 3 outputs - HTML, JS and CSS
    expect(build.outputs).toHaveLength(3);

    // Verify we have one of each type
    const types = build.outputs.map(o => o.type);
    expect(types).toContain("text/html;charset=utf-8");
    expect(types).toContain("text/javascript;charset=utf-8");
    expect(types).toContain("text/css;charset=utf-8");

    // Verify the JS output contains the __dirname
    const js = build.outputs.find(o => o.type === "text/javascript;charset=utf-8");
    expect(await js?.text()).toContain("console.log(3)");

    // Verify our plugin modified the HTML
    const html = build.outputs.find(o => o.type === "text/html;charset=utf-8");
    expect(await html?.text()).toContain("<meta name='injected-by-plugin' content='true'>");
  });
});

test("macro with nested object", async () => {
  const dir = tempDirWithFilesAnon({
    "index.ts": `
import { testMacro } from "./macro" assert { type: "macro" };

export const testConfig = testMacro({
  borderRadius: {
    1: "4px",
    2: "8px",
  },
});
    `,
    "macro.ts": `
export function testMacro(val: any) {
  return val;
}
    `,
  });

  const build = await Bun.build({
    entrypoints: [join(dir, "index.ts")],
    minify: true,
  });

  expect(build.outputs).toHaveLength(1);
  expect(build.outputs[0].kind).toBe("entry-point");
  expect(await build.outputs[0].text()).toEqualIgnoringWhitespace(
    `var t={borderRadius:{"1":"4px","2":"8px"}};export{t as testConfig};\n`,
  );
});

// Since NODE_PATH has to be set, we need to run this test outside the bundler tests.
test("regression/NODE_PATHBuild api", async () => {
  const dir = tempDirWithFiles("node-path-build", {
    "entry.js": `
      import MyClass from 'MyClass';
      console.log(new MyClass().constructor.name);
    `,
    "src/MyClass.js": `
      export default class MyClass {}
    `,
    "build.js": `
      import { join } from "path";
      
      const build = await Bun.build({
        entrypoints: [join(import.meta.dir, "entry.js")],
        outdir: join(import.meta.dir, "out"),
      });
      
      if (!build.success) {
        console.error("Build failed:", build.logs);
        process.exit(1);
      }
      
      // Run the built file
      const runProc = Bun.spawn({
        cmd: [process.argv[0], join(import.meta.dir, "out", "entry.js")],
        stdout: "pipe",
        stderr: "pipe",
      });
      
      await runProc.exited;
      const runOutput = await new Response(runProc.stdout).text();
      const runError = await new Response(runProc.stderr).text();
      
      if (runError) {
        console.error("Run error:", runError);
        process.exit(1);
      }
      
      console.log(runOutput.trim());
      
    `,
  });

  // Run the build script with NODE_PATH set
  const proc = Bun.spawn({
    cmd: [bunExe(), join(dir, "build.js")],
    env: {
      ...bunEnv,
      NODE_PATH: join(dir, "src"),
    },
    stdout: "pipe",
    stderr: "pipe",
    cwd: dir,
  });

  await proc.exited;
  const output = await proc.stdout.text();
  const error = await proc.stderr.text();

  expect(error).toBe("");
  expect(output.trim()).toBe("MyClass");
});

test("regression/GlobalThis", async () => {
  const dir = tempDirWithFiles("global-this-regression", {
    "entry.js": `
      function identity(x) {
        return x;
      }
  import * as mod1 from  'assert';
  identity(mod1);
import * as mod2 from  'buffer';
identity(mod2);
import * as mod3 from  'console';
identity(mod3);
import * as mod4 from  'constants';
identity(mod4);
import * as mod5 from  'crypto';
identity(mod5);
import * as mod6 from  'domain';
identity(mod6);
import * as mod7 from  'events';
identity(mod7);
import * as mod8 from  'http';
identity(mod8);
import * as mod9 from  'https';
identity(mod9);
import * as mod10 from  'net';
identity(mod10);
import * as mod11 from  'os';
identity(mod11);
import * as mod12 from  'path';
identity(mod12);
import * as mod13 from  'process';
identity(mod13);
import * as mod14 from  'punycode';
identity(mod14);
import * as mod15 from  'stream';
identity(mod15);
import * as mod16 from  'string_decoder';
identity(mod16);
import * as mod17 from  'sys';
identity(mod17);
import * as mod18 from  'timers';
identity(mod18);
import * as mod20 from  'tty';
identity(mod20);
import * as mod21 from  'url';
identity(mod21);
import * as mod22 from  'util';
identity(mod22);
import * as mod23 from  'zlib';
identity(mod23);
      `,
  });

  const build = await Bun.build({
    entrypoints: [join(dir, "entry.js")],
    target: "browser",
  });

  expect(build.success).toBe(true);
  const text = await build.outputs[0].text();
  expect(text).not.toContain("process.env.");
  expect(text).not.toContain(" global.");
  expect(text).toContain(" globalThis.");
});

describe("sourcemap boolean values", () => {
  test("sourcemap: true should work (boolean)", async () => {
    const dir = tempDirWithFiles("sourcemap-true-boolean", {
      "index.js": `console.log("hello");`,
    });

    const build = await Bun.build({
      entrypoints: [join(dir, "index.js")],
      sourcemap: true,
    });

    expect(build.success).toBe(true);
    expect(build.outputs).toHaveLength(1);
    expect(build.outputs[0].kind).toBe("entry-point");

    const output = await build.outputs[0].text();
    expect(output).toContain("//# sourceMappingURL=data:application/json;base64,");
  });

  test("sourcemap: false should work (boolean)", async () => {
    const dir = tempDirWithFiles("sourcemap-false-boolean", {
      "index.js": `console.log("hello");`,
    });

    const build = await Bun.build({
      entrypoints: [join(dir, "index.js")],
      sourcemap: false,
    });

    expect(build.success).toBe(true);
    expect(build.outputs).toHaveLength(1);
    expect(build.outputs[0].kind).toBe("entry-point");

    const output = await build.outputs[0].text();
    expect(output).not.toContain("//# sourceMappingURL=");
  });

  test("sourcemap: true with outdir should create linked sourcemap", async () => {
    const dir = tempDirWithFiles("sourcemap-true-outdir", {
      "index.js": `console.log("hello");`,
    });

    const build = await Bun.build({
      entrypoints: [join(dir, "index.js")],
      outdir: join(dir, "out"),
      sourcemap: true,
    });

    expect(build.success).toBe(true);
    expect(build.outputs).toHaveLength(2);

    const jsOutput = build.outputs.find(o => o.kind === "entry-point");
    const mapOutput = build.outputs.find(o => o.kind === "sourcemap");

    expect(jsOutput).toBeTruthy();
    expect(mapOutput).toBeTruthy();
    expect(jsOutput!.sourcemap).toBe(mapOutput!);

    const jsText = await jsOutput!.text();
    expect(jsText).toContain("//# sourceMappingURL=index.js.map");
  });
});

const originalCwd = process.cwd() + "";

describe("tsconfig option", () => {
  afterEach(() => {
    process.chdir(originalCwd);
  });

  test("should resolve path mappings", async () => {
    const dir = tempDirWithFiles("tsconfig-api-basic", {
      "tsconfig.json": `{
        "compilerOptions": {
          "paths": {
            "@/*": ["./src/*"]
          }
        }
      }`,
      "src/utils.ts": `export const greeting = "Hello World";`,
      "index.ts": `import { greeting } from "@/utils";
export { greeting };`,
    });

    try {
      process.chdir(dir);
      const result = await Bun.build({
        entrypoints: ["./index.ts"],
        tsconfig: "./tsconfig.json",
      });
      expect(result.success).toBe(true);
      expect(result.outputs).toHaveLength(1);
      const output = await result.outputs[0].text();
      expect(output).toContain("Hello World");
    } finally {
      process.chdir(originalCwd);
    }
  });

  test("should work from nested directories", async () => {
    const dir = tempDirWithFiles("tsconfig-api-nested", {
      "tsconfig.json": `{
        "compilerOptions": {
          "paths": {
            "@/*": ["./src/*"]
          }
        }
      }`,
      "src/utils.ts": `export const greeting = "Hello World";`,
      "src/nested/index.ts": `import { greeting } from "@/utils";
export { greeting };`,
    });

    try {
      process.chdir(join(dir, "src/nested"));
      const result = await Bun.build({
        entrypoints: ["./index.ts"],
        tsconfig: "../../tsconfig.json",
      });
      expect(result.success).toBe(true);
      expect(result.outputs).toHaveLength(1);
      const output = await result.outputs[0].text();
      expect(output).toContain("Hello World");
    } finally {
      process.chdir(originalCwd);
    }
  });

  test("should handle relative tsconfig paths", async () => {
    const dir = tempDirWithFiles("tsconfig-api-relative", {
      "tsconfig.json": `{
        "compilerOptions": {
          "baseUrl": ".",
          "paths": {
            "@/*": ["src/*"]
          }
        }
      }`,
      "configs/build-tsconfig.json": `{
        "extends": "../tsconfig.json",
        "compilerOptions": {
          "baseUrl": ".."
        }
      }`,
      "src/utils.ts": `export const greeting = "Hello World";`,
      "index.ts": `import { greeting } from "@/utils";
export { greeting };`,
    });

    try {
      process.chdir(dir);
      const result = await Bun.build({
        entrypoints: ["./index.ts"],
        tsconfig: "./configs/build-tsconfig.json",
      });
      expect(result.success).toBe(true);
      expect(result.outputs).toHaveLength(1);
      const output = await result.outputs[0].text();
      expect(output).toContain("Hello World");
    } finally {
      process.chdir(originalCwd);
    }
  });

  test("onEnd fires before promise resolves with throw: true", async () => {
    const dir = tempDirWithFiles("onend-throwonerror-true", {
      "index.ts": `
        // This will cause a build error
        import { missing } from "./does-not-exist";
        console.log(missing);
      `,
    });

    let onEndCalled = false;
    let onEndCalledBeforeReject = false;
    let promiseRejected = false;

    try {
      await Bun.build({
        entrypoints: [join(dir, "index.ts")],
        throw: true,
        plugins: [
          {
            name: "test-plugin",
            setup(builder) {
              builder.onEnd(result => {
                onEndCalled = true;
                onEndCalledBeforeReject = !promiseRejected;
                // Result should contain error information
                expect(result.success).toBe(false);
                expect(result.logs).toBeDefined();
                expect(result.logs.length).toBeGreaterThan(0);
              });
            },
          },
        ],
      });
      // Should not reach here
      expect(false).toBe(true);
    } catch (error) {
      promiseRejected = true;
      // Verify onEnd was called before promise rejected
      expect(onEndCalled).toBe(true);
      expect(onEndCalledBeforeReject).toBe(true);
    }
  });

  test("onEnd fires before promise resolves with throw: false", async () => {
    const dir = tempDirWithFiles("onend-throwonerror-false", {
      "index.ts": `
        // This will cause a build error
        import { missing } from "./does-not-exist";
        console.log(missing);
      `,
    });

    let onEndCalled = false;
    let onEndCalledBeforeResolve = false;
    let promiseResolved = false;

    const result = await Bun.build({
      entrypoints: [join(dir, "index.ts")],
      throw: false,
      plugins: [
        {
          name: "test-plugin",
          setup(builder) {
            builder.onEnd(result => {
              onEndCalled = true;
              onEndCalledBeforeResolve = !promiseResolved;
              // Result should contain error information
              expect(result.success).toBe(false);
              expect(result.logs).toBeDefined();
              expect(result.logs.length).toBeGreaterThan(0);
            });
          },
        },
      ],
    });

    promiseResolved = true;

    // Verify onEnd was called before promise resolved
    expect(onEndCalled).toBe(true);
    expect(onEndCalledBeforeResolve).toBe(true);
    expect(result.success).toBe(false);
    expect(result.logs.length).toBeGreaterThan(0);
  });

  test("onEnd always fires on successful build", async () => {
    const dir = tempDirWithFiles("onend-success", {
      "index.ts": `
        export const message = "Build successful";
        console.log(message);
      `,
    });

    let onEndCalled = false;
    let onEndCalledBeforeResolve = false;
    let promiseResolved = false;

    const result = await Bun.build({
      entrypoints: [join(dir, "index.ts")],
      throw: true, // Should not matter for successful build
      plugins: [
        {
          name: "test-plugin",
          setup(builder) {
            builder.onEnd(result => {
              onEndCalled = true;
              onEndCalledBeforeResolve = !promiseResolved;
              // Result should indicate success
              expect(result.success).toBe(true);
              expect(result.outputs).toBeDefined();
              expect(result.outputs.length).toBeGreaterThan(0);
            });
          },
        },
      ],
    });

    promiseResolved = true;

    // Verify onEnd was called before promise resolved
    expect(onEndCalled).toBe(true);
    expect(onEndCalledBeforeResolve).toBe(true);
    expect(result.success).toBe(true);
    const output = await result.outputs[0].text();
    expect(output).toContain("Build successful");
  });

  test("multiple onEnd callbacks fire in order before promise settles", async () => {
    const dir = tempDirWithFiles("onend-multiple", {
      "index.ts": `
        // This will cause a build error
        import { missing } from "./not-found";
      `,
    });

    const callOrder: string[] = [];
    let promiseSettled = false;

    const result = await Bun.build({
      entrypoints: [join(dir, "index.ts")],
      throw: false,
      plugins: [
        {
          name: "plugin-1",
          setup(builder) {
            builder.onEnd(() => {
              callOrder.push("first");
              expect(promiseSettled).toBe(false);
            });
          },
        },
        {
          name: "plugin-2",
          setup(builder) {
            builder.onEnd(() => {
              callOrder.push("second");
              expect(promiseSettled).toBe(false);
            });
          },
        },
        {
          name: "plugin-3",
          setup(builder) {
            builder.onEnd(() => {
              callOrder.push("third");
              expect(promiseSettled).toBe(false);
            });
          },
        },
      ],
    });

    promiseSettled = true;

    // All callbacks should have fired in order before promise resolved
    expect(callOrder).toEqual(["first", "second", "third"]);
    // The build actually succeeds because the import is being resolved to nothing
    // What matters is that callbacks fired before promise settled
    expect(result.success).toBeDefined();
  });
});
